/*
 * TCSS 422 Web Spider Project
 * Group Money: Al McKenzie, Michael Pitts, Taylor Zielske
 */
package control;

import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;

import model.URLInputCooker;
import model.URLStore;
import model.domain.BlockDomain;

import control.data.SearchHopper;
import control.threads.ComplexMultiThreadDirector;
import control.threads.SimpleMultiThreadDirector;
import control.threads.SingleThreadDirector;
import control.threads.ThreadDirector;

import gui.FinishGUI;
import gui.RunningGUI;
import gui.StartGUI;

/**
 * This is the main controlling class for the web crawler.  It will feed input 
 * and start and stop processes.
 * 
 * @author Michael Pitts
 * @version Nov 9, 2011
 */
public class MainControl implements Observer{
	
	/**
	 * The message given if the end of the network is found but not the max number
	 * of sites searched.
	 */
	private static final String END = "Reached the end of the seeded network";
	
	/**
	 * The SearchHopper for the program, which stores the results of the searches.
	 */
	private final SearchHopper my_hopper;
	
	/**
	 * The Thread director for the program, which controls how many threads are 
	 * distributed to each part of the program.
	 */
	private final ThreadDirector my_director;

	/**
	 * The URLStore for the program, which stores potential and visited urls.
	 */
	private final URLStore my_store;
	
	/**
	 * The RunningGUI for the program, which displays output as the program runs.
	 */
	private RunningGUI my_running_gui;
	
	/**
	 * If true, runs the running and finish gui, if false only start gui.
	 */
	private boolean my_run_gui;
	
	/**
	 * Catches the print streams that will be used to output data.
	 */
	private final PrintStream[] my_streams;
	
	/**
	 * The final gui to display.
	 */
	private FinishGUI my_finish_gui;
	
	/**
	 * Catches update data.
	 */
	private Object[] my_passed;
	
	/**
	 * Flagged true if a final message has been sent or not (to keep it thread safe).
	 */
	private boolean my_message_sent = false;
	
	/**
	 * Creates a new instance of MainControl, which can then crawl the web.
	 */

	public MainControl() {
		final UserSettings settings = getSettings();
		final Collection<String> words = settings.getSearchTerms();
		my_hopper = new SearchHopper(words.toArray(new String[words.size()]), 
				settings.getMaxSites(), settings.getPrintStreams(), settings.getUpdateFrequency());
		final URLInputCooker cooker = new URLInputCooker();
		cooker.addObserver(this); // in case the network runs out before max is reached
		my_store = new URLStore(cooker);
		my_director = getThreadDirector(cooker, my_hopper, my_store, 
				settings.getSearchTerms().
				toArray(new String[settings.getSearchTerms().size()]), 
				settings.getNumberThreads());
		guiSetUp(settings);
		my_streams = settings.getPrintStreams();
		loadSeeds(settings);
	}
	
	/**
	 * Sets up the future gui use based on the weather the user selected gui output
	 * or not.
	 * @param the_settings are the user settings generated by the user.
	 */
	private void guiSetUp(final UserSettings the_settings) {
		my_run_gui = the_settings.getDisplayGUI(); // checks if the user wants the gui
		if (my_run_gui) {
			my_running_gui = new RunningGUI();
		} else {
			my_running_gui = null;
		}
	}
	
	/**
	 * Gets the user settings from an instance of the StartGUI class, which
	 * brings up a window and allows the use to select the run time options.
	 * @return an instance of the UserSettings class, which has captured the
	 * user's preferences and initial input.
	 */
	private UserSettings getSettings() {
		final StartGUI start_gui = new StartGUI(UserSettings.getDefaultSettings());
		start_gui.start();
		final UserSettings settings =  start_gui.getData(); // get data from gui
		start_gui.stop();
		return settings;
	}
	
	/**
	 * Gets the ThreadDirector for this program, depending on the thread allocation
	 * specified by the user in the_number_threads
	 * @param the_cooker is the URLInputCooker for this program.
	 * @param the_hopper is the SearchHopper for this program.
	 * @param the_store is the URLStore for this program.
	 * @param the_words are the words to search for during the run of this program.
	 * @param the_number_threads is an array of integers that represent how the threads
	 * are allocated.  If it is null, or only has 1 as a value single threaded.  If
	 * only one value greater than one simple multi-threaded.  Else threads are divided
	 * up between buffering and parsing.
	 * @return a new ThreadDirector that works based on how the threads are assigned.
	 */
	private ThreadDirector getThreadDirector(final URLInputCooker the_cooker,
			final SearchHopper the_hopper, final URLStore the_store, 
			final String[] the_words, final int[] the_number_threads) {
		final ThreadDirector td;
		if (the_number_threads == null || 
				(the_number_threads.length == 1 && the_number_threads[0] == 1)) {
			td = new SingleThreadDirector(the_cooker, the_hopper, the_store, 
					the_words);
		} else if (the_number_threads.length == 1) {
			td = new SimpleMultiThreadDirector(the_cooker, the_hopper, the_store, 
					the_words, the_number_threads[0]);
		} else {
			td = new ComplexMultiThreadDirector(the_cooker, the_hopper, the_store, 
					the_words, the_number_threads);
		}
		return td;
	}
	
	/**
	 * Loads the banned and seed urls into the 
	 * @param the_settings are the user settings which contain the seed and banned
	 * url strings.
	 */
	private void loadSeeds(final UserSettings the_settings) {
		URL temp;
		for (String banned : the_settings.getBannedLinks()) {
			try {
				temp = new URL(banned);
				my_store.replaceDomain(new BlockDomain(temp.getHost(), null ));
			} catch (MalformedURLException the_error) {
				// bad reference, ignore
				//the_error.printStackTrace();
			}
		}
		for (String seed : the_settings.getSeeds()) {
			try {
				my_store.addURL(new URL(seed));
			} catch (MalformedURLException the_exception) {
				// If bad seed , ignore.
			}
		}
	}

	/**
	 * Starts the web crawler, bringing up a GUI and when input has been taken
	 * begins to crawl the web.
	 */
	public void start() {
		if (my_run_gui) { // checks if the user wants the gui
			my_running_gui.start();
			my_hopper.addObserver(my_running_gui);
		}
		my_hopper.addObserver(this); // get notified when max is reached
		my_director.start();
	}

	/**
	 * Starts the web crawler program.
	 * @param the_args are not used.
	 * @throws FileNotFoundException 
	 */
	public static void main(final String[] the_args) throws FileNotFoundException {
		new MainControl().start();
	}

	/**
	 * Run when the SearchHopper sends out an update.
	 * @param the_observable must be the SearchHopper, otherwise the update is ignored.
	 * @param the_arg must be an Integer, otherwise the update is ignored.
	 */
	@Override public void update(final Observable the_observable, Object the_arg) {
		if (my_hopper.equals(the_observable) && Integer.class.isInstance(the_arg) && 
				((Integer)the_arg).intValue() == SearchHopper.STOP) {
			stop(null);// max has been reached
		} else if (Object[].class.isInstance(the_arg)){
			my_passed = (Object[]) the_arg; // store for final gui display
		} else if (URLInputCooker.class.isInstance(the_observable)) {
			stop(END); // network too small, ended before max sites searched.
		}
	}

	/**
	 * Stops the web spider, displaying the final results.
	 * @param the_message is the string message to send on finishing, in case of an
	 * error.
	 */
	private synchronized void stop(final String the_message) {
		my_director.stop();
		sendMessage(the_message);
		if (my_run_gui) { // checks if the user wanted gui output
			my_running_gui.stop();
			if (my_finish_gui == null) { // for multiple threads, make sure only one stop
				my_finish_gui = new FinishGUI();
				my_finish_gui.start();
				if (my_passed != null) {
					my_finish_gui.inputData(my_passed);
				}
			}
		}
	}
	
	/**
	 * Sends out the given message to all print streams once.  Thread safe.
	 * @param the_message is the message to send.  If null, no message is sent.
	 */
	private synchronized void sendMessage(final String the_message) {
		if (!my_message_sent && the_message != null && my_streams != null) {
			my_message_sent = true;
			for(PrintStream stream : my_streams) {
				stream.println(the_message);
			}
		}
	}
}
